
float ClipByFrustum(Ray ray)
{
	float t = uintBitsToFloat(0x7f7fffff);
	for (uint k = 0; k < 6; ++k)
	{
		ClipByPlane(ray, cb_cbFrustumPlanes.View[k], t);
	}
	//
	return t;
}

#ifdef SSR_ZHierarchyRayMarshing

vec2 raytraceBruteForce(in vec2 uv0, in vec2 uv1, in float z0, in float z1, out vec3 hitPos, out bool hit)
{
  float invZ0 = 1.0 / z0;
  float invZ1 = 1.0 / z1;

  // We don't use custom Z map on this path because perfomance gain is not high enough and quality drop is really bad.
  vec2 rtSize = vec2(1.0 / cb_PostEffectAuto.u_PixelSize.x, 1.0 / cb_PostEffectAuto.u_PixelSize.y);
  vec2 uvDir = uv1 - uv0;
  float uvLength = vLength(uvDir);
  vec2 dirSteps = vAbs(rtSize * uvDir / uvLength);
	uint nbSteps = uint(vMax(dirSteps.x, dirSteps.y));
  nbSteps = vMax(1, nbSteps);

  vec2 uv;
  vec3 geomPos;
  float geomBiasedPosZ;
  float rayPosZ;
  int mipLvl = 0;
  int powMip = 1;
  int k = 0;
  float ratio = 0.0;

  while ( mipLvl > -1 && k < nbSteps )
  {
    k += powMip;
    ratio = float(k) / float(nbSteps-1);
    uv = vMix(uv0, uv1, ratio);

#ifdef PROJECTION_CONIC
    float iz = vMix(invZ0, invZ1, ratio);
    rayPosZ = 1.0 / iz;
#else
    float z = vMix(z0, z1, ratio);
    rayPosZ = z;
#endif

    geomPos = UVToViewPosMip(uv, mipLvl);
    geomBiasedPosZ = geomPos.z * cb_PostEffect_SSR_Radiance.u_RayZBias;

    // Readable
    /*if ( rayPosZ < geomBiasedPosZ )
    {
      k -= powMip;
      mipLvl--;
      powMip /= 2;
    }
    else
    {
      mipLvl++;
      powMip *= 2;
    }*/

    // Optimized
    float state = vStep(geomBiasedPosZ, rayPosZ);
    k -= int((1.0-state)*float(powMip));
    mipLvl += int(vMix(-1.0, 1.0, state));
    float newPowMip = float(powMip) * vMix(0.5, 2.0, state);
    powMip = int(newPowMip);
  }

  if ( k >= nbSteps || vAbs(1.0 - rayPosZ / geomBiasedPosZ) > cb_PostEffect_SSR_Radiance.u_DepthDiscontinuity )
  {
    hit = false;
    return vec2(0.0);
  }

  float z = vTextureLod(ZMapWithMips, uv, 0.0).x;
  if ( z < 0.0001 || z > 0.9999 )
  {
    hit = false;
    return vec2(0.0);
  }

  hitPos = geomPos;
  hit = true;
  return uv;
}

#endif

vec2 raytraceBruteForce(in float prec, in vec2 uv0, in vec2 uv1, in float z0, in float z1, out vec3 hitPos, out bool hit)
{
  float invZ0 = 1.0 / z0;
  float invZ1 = 1.0 / z1;

  vec2 rtSize = vec2(1.0 / cb_PostEffect_SSR_Radiance.u_ZMapPixelSize.x, 1.0 / cb_PostEffect_SSR_Radiance.u_ZMapPixelSize.y);
  vec2 uvDir = uv1 - uv0;
  float uvLength = vLength(uvDir);
  vec2 dirSteps = vAbs(rtSize * uvDir / uvLength);
	uint nbSteps = uint(prec * vMax(dirSteps.x, dirSteps.y));
  nbSteps = vMax(1, nbSteps);
  
  uint kStart = 1;
  uint kEnd = nbSteps;
  vec2 uv;
  uint k;
  float rayPosZ;
  vec3 geomPos;
  float geomBiasedPosZ;

#ifdef SSR_LowResRayMarshing
  for (k = cb_PostEffect_SSR_Radiance.u_RayMarchingLowRes; k < kEnd; k += cb_PostEffect_SSR_Radiance.u_RayMarchingLowRes)
	{
    float ratio = float(k) / float(nbSteps-1);
    uv = vMix(uv0, uv1, ratio);

#ifdef PROJECTION_CONIC
    float iz = vMix(invZ0, invZ1, ratio);
    rayPosZ = 1.0 / iz;
#else
    float z = vMix(z0, z1, ratio);
    rayPosZ = z;
#endif

    geomPos = UVToViewPosLowRes(uv);
    geomBiasedPosZ = geomPos.z * cb_PostEffect_SSR_Radiance.u_RayZBias;

    if ( rayPosZ < geomBiasedPosZ )
    {
      kStart = k-cb_PostEffect_SSR_Radiance.u_RayMarchingLowRes;
      break;
    }
  }

  kEnd = kStart+cb_PostEffect_SSR_Radiance.u_RayMarchingLowRes+2;
#endif

	for (k = kStart; k < kEnd; ++k)
	{
    float ratio = float(k) / float(nbSteps-1);
    uv = vMix(uv0, uv1, ratio);

#ifdef PROJECTION_CONIC
    float iz = vMix(invZ0, invZ1, ratio);
    rayPosZ = 1.0 / iz;
#else
    float z = vMix(z0, z1, ratio);
    rayPosZ = z;
#endif

#ifdef SSR_CustomZMap
    geomPos = UVToViewPosCustom(uv);
#else
    geomPos = UVToViewPos(uv);
#endif
    geomBiasedPosZ = geomPos.z * cb_PostEffect_SSR_Radiance.u_RayZBias;
    
    if ( rayPosZ < geomBiasedPosZ )
    {
      if (vAbs(1.0 - rayPosZ / geomBiasedPosZ) > cb_PostEffect_SSR_Radiance.u_DepthDiscontinuity)
      {
        hit = false;
        return vec2(0.0);
      }
      break;
    }
  }

  if ( k < kEnd )
  {
#ifdef SSR_CustomZMap
    float z = vTexture(CustomZMap, uv).x;
#else
    float z = GetZ(uv);
#endif
    if ( z < 0.0001 || z > 0.9999 )
    {
      hit = false;
      return vec2(0.0);
    }
    hitPos = geomPos;
    hit = true;
    return uv;
  }

  hit = false;
  return vec2(0.0);
}

void main()
{
  vec4 f0Rough = GetEffectTexture(F0RoughnessMap, vTexCoord.xy);
  vec3 f0 = f0Rough.rgb;
	float roughness = f0Rough.a;	
	if (roughness > 0.99)
  {
    FRAGCOLOR = vec4(0.0);
    return;
  }
  roughness = vMax(roughness, 0.025);

  vec3 normal = GetNormal(vTexCoord.xy).xyz;
	if (vDot(normal, normal) < 0.01)
  {
    FRAGCOLOR = vec4(0.0);
    return;
  }

  vec3 viewVect = UVToViewVect(vTexCoord.xy);
  
  vec3 specular = OptimizedSchlickFresnel(f0, vec3(1.0), vAbs(vDot(normal, viewVect)));
	if (vDot(specular, specular) < 0.0001)
  {
    FRAGCOLOR = vec4(0.0);
    return;
  }

  //int mipTst = 5;
  //float z = vTextureLod(ZMapWithMips, vTexCoord.xy, float(mipTst)).x;
  //FRAGCOLOR = vec4(vec3(z)*0.1, 1.0);
  //FRAGCOLOR = vec4(normal, 1.0);
  //return;

  vec2 mainNoiseUV = vTexCoord.xy * cb_PostEffectAuto.u_ViewpointSize.xy / 128.0;

  vec4 radColor = vec4(0.0);
#ifdef SSR_MultipleRayCast
  const int rayCastCount = 1+int(roughness * float(cb_PostEffect_SSR_Radiance.u_MaxRayCastCount));
  const float rayLengthRate = vSqrt(1.0 - float(rayCastCount) / float(cb_PostEffect_SSR_Radiance.u_MaxRayCastCount));
  const float prec = cb_PostEffect_SSR_Radiance.u_RayMarchingPrecision / float(rayCastCount);
  for ( int i = 0 ; i < rayCastCount ; i++ )
  {
  vec2 roughNoiseUV = gl_FragCoord.xy / float(i+1);
#else
  const float rayLengthRate = 1.0;
  const float prec = cb_PostEffect_SSR_Radiance.u_RayMarchingPrecision;
  vec2 roughNoiseUV = vec2(0.0);
#endif

#ifdef SSR_GroundBE
  const float sceneSize = 2.0 * cb_Ground_Auto.u_GroundRadius * rayLengthRate;
#else
  const float sceneSize = 2.0 * cb_PostEffectAuto.u_SceneRadius * rayLengthRate;
#endif
  const float maxRayLength = sceneSize * cb_PostEffect_SSR_Radiance.u_SceneRadiusRateFalloff.y;

	float noise = vTexture(NoiseMap, mainNoiseUV+roughNoiseUV).r;

	vec2 xi = vec2(0.0); // random variable for microfacet distribution		
	vec2 wrapUV = 128.0 * (mainNoiseUV+roughNoiseUV);

  const int wrap = cb_PostEffect_SSR_Radiance.u_GGXNoiseRepeat;
	
	wrapUV.x = float(int(wrapUV.x) % wrap);
	wrapUV.y = float(int(wrapUV.y) % wrap);
	
	wrapUV *= 1.0 / 128.0;
	
	xi.x = vTexture(NoiseMap, wrapUV + vec2(0.55324, 0.25648)).r;
	xi.y = vTexture(NoiseMap, wrapUV + vec2(0.57584, 0.725148)).r;

  vec3 H = ImportanceSampleGGX(xi, roughness, normal);
  vec3 reflVect = vReflect(viewVect, H);

  float RoN = vDot(reflVect, normal);
  if (RoN < 0.0) reflVect = reflVect - 2.0 * normal * RoN;

	Ray ray;
  {
    ray.Dir = reflVect;
  #ifdef SSR_GroundCorrection
    float z = GetZ(vTexCoord.xy);
    if ( z < 0.0001 || z > 0.9999 )
    {
      // Compute ray from ground plane
      ray.Start = GetViewRayPlaneInter(vTexCoord.xy, viewVect, cb_GroundPlane.ViewEquation);
    #ifdef SSR_GroundInDepth
      // Compute ray from near/far plane (when "useGroundBe" is disabled)
      float inter = ClipByFrustum(ray);
      ray.Start += 1.001 * inter * ray.Dir;
    #endif
    }
    else
  #endif
    {
      ray.Start = UVToViewPos(vTexCoord.xy);
    }
  }

	float t = ClipByFrustum(ray);

	vec3 hitPos;
  vec2 hitUV;
	bool hit = false;
  if ( t > 0.0 )
  {
	  vec3 outPos = ray.Start + vMin(t, maxRayLength) * ray.Dir;
	  vec2 uv0 = ViewPosToUV(ray.Start);
	  vec2 uv1 = ViewPosToUV(outPos);

  #ifdef SSR_ZHierarchyRayMarshing
    // Z hierarchy algorithm doesn't scale with precision
  #ifdef SSR_MultipleRayCast
    if ( rayCastCount < 2 )
	    hitUV = raytraceBruteForce(uv0, uv1, ray.Start.z, outPos.z, hitPos, hit);
    else
      hitUV = raytraceBruteForce(prec, uv0, uv1, ray.Start.z, outPos.z, hitPos, hit);
  #else
	  hitUV = raytraceBruteForce(uv0, uv1, ray.Start.z, outPos.z, hitPos, hit);
  #endif
  #else
	  hitUV = raytraceBruteForce(prec, uv0, uv1, ray.Start.z, outPos.z, hitPos, hit);
  #endif
  }

  if ( hit )
  {
  #ifdef SSR_RadianceNoMip
    vec3 hitRadiance = GetColor(hitUV).xyz;
  #else
    float mipLvl = roughness * float(cb_PostEffect_SSR_Radiance.u_EyeMapMipsCount);
    vec3 hitRadiance = vTextureLod(EyeMapWithMips, hitUV, mipLvl).xyz;
  #endif

	  radColor.rgb += specular * hitRadiance;

    vec3 rayTracePath = hitPos - ray.Start;
    float rayTraceLength = vLength(rayTracePath);
    float sceneRateRayTraceLength = rayTraceLength / sceneSize;
    const float sceneRateFallbackRange = rayLengthRate * (cb_PostEffect_SSR_Radiance.u_SceneRadiusRateFalloff.y - cb_PostEffect_SSR_Radiance.u_SceneRadiusRateFalloff.x);
    float sceneFalloff = vSaturate(1.0 - (sceneRateRayTraceLength - rayLengthRate * cb_PostEffect_SSR_Radiance.u_SceneRadiusRateFalloff.x) / sceneRateFallbackRange);

    vec2 boundary = vAbs(hitUV - vec2(0.5, 0.5)) * 2.0;
    float screenFalloff = 1.0 - vSaturate((boundary.x - cb_PostEffect_SSR_Radiance.u_ScreenBorderRateFalloff.x) / (1.0-cb_PostEffect_SSR_Radiance.u_ScreenBorderRateFalloff.x));
    screenFalloff *= 1.0 - vSaturate((boundary.y - cb_PostEffect_SSR_Radiance.u_ScreenBorderRateFalloff.y) / (1.0-cb_PostEffect_SSR_Radiance.u_ScreenBorderRateFalloff.y));

    radColor.a += screenFalloff * sceneFalloff;
  }
#ifdef SSR_MultipleRayCast
  } // "for" loop end
  radColor /= float(rayCastCount);
#endif

  // Quick hack to drasticly reduce "fireflies" effect
  radColor.rgb = vMin(radColor.rgb, vec3(1.0));

  FRAGCOLOR = radColor;
}
